Инструмент для просмотра машинного кода .net и исполняемых файлов
https://github.com/dnSpyEx/dnSpy/releases/tag/v6.5.1
Очень удобно
Каждая секция содержит в начале поля:
Общая структура файла такая:
#~ или #- |
Таблицы метаданных (compressed / uncompressed формат) |
---|---|
#Strings |
Таблица строк (имена классов, методов и пр.) |
#US |
User Strings (строковые литералы из IL) |
#Blob |
Сигнатуры методов, массивы, параметры |
#GUID |
GUID'ы сборки |
Отдельно про поток #~
в нем хранятся таблицы метаданных
Каждая таблица — это структурированный набор записей, похожий на строки базы данных. Каждая строка описывает что-то: тип, метод, атрибут и т.д.
Таблица | Назначение |
---|---|
00 Module | Информация о текущем модуле (сборке .dll/.exe). Одна строка. |
01 TypeRef | Ссылки на типы, определённые в других сборках. |
02 TypeDef | Определения всех типов в этой сборке. |
06 MethodDef | Все методы, определённые в сборке. |
08 Param | Описания параметров методов. |
0A MemberRef | Ссылки на поля/методы, определённые вне текущей сборки. |
0C CustomAttribute | Все кастомные атрибуты (например, [Serializable] ). |
11 StandAloneSig | Сигнатуры (например, для лямбд или delegate). |
1B TypeSpec | Специализации generic-типов (например, List<int> ). |
20 Assembly | Информация о сборке (имя, версия и пр.). |
23 AssemblyRef | Ссылки на внешние сборки. |
2B MethodSpec | Generic-специализации методов (Do<T>() ). |
Для обращения к метаданных из IL кода применяются токены
формат токена
Токен = [таблица или поток (8 бит) ] + [индекс для таблиц/смещение для потока со строками(24 бита)]
Таблицы представлены буквально как таблицы в постгре, то есть идут записи подряд
структура каждой таблицы метаданных в потоке #~
жёстко задана стандартом ECMA-335 — то есть она не описывается в самом PE-файле
Пример структуры таблицы:
TypeDef Row Layout (обычно):
В .Net домены приложений, AppDomains - это изначально механизм изоляции выполнения программ внутри одного процесса. Данный механизм реализовывался на уровне CLR.
AppDomain - это логически изолированная среда. Она:
Осталось:
Что удалено:
Применяются отдельные процессы и AssemblyLoadContext()
Теперь таким образом можно создавать свои контексты сборки и загружать туда длл в виде файла.
пример кода с загрузкой длл
var context = new AssemblyLoadContext("PluginContext", isCollectible: true);
var assembly = context.LoadFromAssemblyPath("/plugins/MyPlugin.dll");
var type = assembly.GetType("MyPluginNamespace.MyPluginClass");
var instance = Activator.CreateInstance(type);
Процесс выгрузки сборки, сработает только если isCollectible: true
var context = new AssemblyLoadContext("UnloadableContext", isCollectible: true);
Assembly assembly = context.LoadFromAssemblyPath("/path/to.dll");
// Работа с типами...
context.Unload(); // Помечает на выгрузку
GC.Collect(); GC.WaitForPendingFinalizers(); // Чтобы реально выгрузилось
⚠️ Важно: выгрузка работает только при отсутствии живых ссылок на типы из загруженной сборки.
По умолчанию .NET использует AssemblyLoadContext.Default
.
Когда вы создаёте новый AssemblyLoadContext
, CLR создает изолированное пространство загрузки, где типы и сборки не пересекаются с другими контекстами.
Может быть сборкой-разгружаемым (isCollectible: true
) или обычным (в памяти навсегда).
Характеристика | AppDomain (.NET Framework) | AssemblyLoadContext (.NET Core / .NET 5+) |
---|---|---|
Изоляция памяти | Частичная — данные и стеки изолированы | Только изоляция сборок (DLL), память общая |
Изоляция объектов | Полная: нельзя передать объекты напрямую | Нет: объекты общие, если типы совпадают |
Механизм безопасности (CAS херня для безопасности кода из винды) | Поддерживает CAS (ограничения прав) | Не поддерживается |
Граница сериализации | Да (можно передавать только сериализуемые) | Нет: объект = объект |
Сборка мусора | Уничтожается вся область домена | Выгружается только если isCollectible = true |
Тип определяется не только по имени, но и по контексту загрузки сборки.
Домен это мега изоляция почти как процессы, но на уровне CLR, но все это обрезали в новых версиях
В рамках одного процесса может быть несколько контекстов, в рамках одного контекста свои переменные и типы. Типы и переменные из двух разных контекстов взаимодействовать друг с другом не могут.
Но! В рамках одного контекста может быть несколько сборок (То есть DLL), и переменные между ними могут взаимодействовать, но нужно учитывать что описание типа - не только название но и сборка. Поэтому, например для каста между разными сборками, можно сделать сборку contracts.dll где описать общие для этих двоих интерфейсы и касты между ними.
Пример кастов между сборками в рамках одного контекста:
var context = new AssemblyLoadContext("MyContext");
var contracts = context.LoadFromAssemblyPath("Contracts.dll");
var pluginA = context.LoadFromAssemblyPath("PluginA.dll");
var pluginB = context.LoadFromAssemblyPath("PluginB.dll");
var typeA = pluginA.GetType("PluginA.MyPlugin");
var typeB = pluginB.GetType("PluginB.MyPlugin");
var objA = Activator.CreateInstance(typeA);
var objB = Activator.CreateInstance(typeB);
// Получаем Type интерфейса из загруженной Contracts.dll
var interfaceType = contracts.GetType("Contracts.IPlugin");
// ✅ Кастинг сработает, потому что это один и тот же Type
bool isAPlugin = interfaceType.IsInstanceOfType(objA); // true
bool isBPlugin = interfaceType.IsInstanceOfType(objB); // true
// Можно привести:
var plugin = (dynamic)objA;
plugin.Run(); // если метод Run есть
Существование контекстов обусловлено системами, которые могут одновременно использовать библиотеки разных версий, и чтобы изолировать типы этих библиотек, их можно подключать в разных контекстах.
Commited память включает в себя ту память, которую ОС выложила на реальную, но возможно на swap возможно в рам
Private WS это память которая выложена в именно в РАМ и которая не общая
Shareable (около 2 ГиБ) – разделяемая память, которая нас не особенно интересует; Эти области служат для целей системного управления, вообще не имеющих отношения к .NET;
Mapped File (около 4 МиБ) – как отмечалось в главе 2, эти области содержат проецируемые файлы, в частности шрифты и файлы локализации. Хотя они и читаются средой выполнения .NET с применением различных API локализации, никаких проблем нашему приложению они создавать не должны;
Image (около 37 МиБ) – двоичные образы, содержащие различные исполняемые файлы .NET, включая саму среду выполнения и нашу сборку. Отметим, что большая часть этой области разделяемая, и лишь 772 КиБ входят в частный рабочий набор. Это файлы, которые читаются с диска на этапе запуска приложения;
Stack (около 4,5 МиБ) – в нашем приложении Hello World три потока, поэтому для них отведено три области под стеки;
Heap и Private Data (около 9 МиБ) – это различные области памяти, которые среда выполнения .NET использует для собственных целей. Среди них есть фундаментальные структуры данных например:
Если по какой-то причине в нашем приложении часто производится JIT-компиляция, то мы будем наблюдать постоянный рост таких частных областей с флагами Выполнение/Чтение/ Запись;
Page Table (небольшая область размером 36 КиБ) – таблица страниц